export namespace TreeNode {
    export declare interface IWeakNodeData {
        id?: number
        displayName: string
        description?: string
        expression: string
        checked: boolean
    }

    export declare interface INodeData extends IWeakNodeData {
        id: number
        description: string
    }

    export declare interface ITreeNodeContent {
        path: string
        data?: IWeakNodeData
        children: ITreeNodeContent[]
    }

    export declare interface ITreeNode extends ITreeNodeContent, Iterable<ITreeNode> {
        parent?: ITreeNode
        toJSON: (() => ITreeNodeContent)

        addData(...nodeData: IWeakNodeData[]): void

        matchNode(expression: string): ITreeNode | null
    }

    export declare interface Route {
        path: string
        name: string
        children?: Route[]
    }

    export function isWildcard(treeNodeContent: ITreeNodeContent): boolean {
        return treeNodeContent.path === '*'
    }

    export function asContent(node: ITreeNode): ITreeNodeContent {
        return {
            path: node.path,
            data: node.data,
            children: node.children.map(e => asContent(e as ITreeNode))
        }
    }

    export function createNode(path: string, parent?: ITreeNode, content?: IWeakNodeData): ITreeNode {
        return createNodeFromContent({
            path,
            data: content,
            children: []
        }, parent)
    }

    export function isNode(node: ITreeNodeContent): node is ITreeNode {
        return Object.getOwnPropertyDescriptor(node, 'parent') !== undefined
    }

    export function createNodeFromContent(content: ITreeNodeContent, parent?: ITreeNode): ITreeNode {
        return {
            ...content,
            [Symbol.iterator](): Iterator<ITreeNode> {
                return (this.children as ITreeNode[])[Symbol.iterator]()
            },
            parent: parent,
            toJSON() {
                return asContent(this)
            },
            addData(...nodeData: IWeakNodeData[]) {
                return appendNodeData(this, ...nodeData)
            },
            matchNode(expression: string): TreeNode.ITreeNode | null {
                return matchNode(this, expression.split('.'))
            }
        }
    }

    export function matchNode(node: ITreeNode, expression: string[], currentFlow: number = 0): ITreeNode | null {
        if (expression.length === 0) {
            return null
        }
        if (currentFlow === expression.length) {
            return node
        }
        const currentPath = expression[currentFlow]
        if (currentPath === '*') {
            return node
        }
        for (const child of node.children) {
            if (child.path == currentPath) {
                const matched = matchNode(child as ITreeNode, expression, currentFlow + 1)
                if (matched) {
                    return matched
                }
            }
        }
        return null
    }

    export function createRoot() {
        return createNode("<root>")
    }

    export function appendNodeData(treeNode: ITreeNode, ...nodeData: IWeakNodeData[]) {
        nodeData.forEach(data => {
            let current: ITreeNodeContent = treeNode
            const paths = data.expression.split('.')
            paths.forEach((path, idx) => {
                if (path === "*") {
                    if (idx !== paths.length - 1) {
                        throw new Error(`Wildcard can only be used at the end of the expression.`)
                    }
                    current.data = data
                } else {
                    const exists = current.children.find(child => child.path === path)
                    if (exists) {
                        current = exists
                    } else {
                        const newNode = createNode(path, current as ITreeNode, (idx === paths.length - 1) ? data : undefined)
                        current.children.push(newNode)
                        current = newNode
                    }
                }
            })
        })
    }


    function getNodePath(treeNode: ITreeNode): string {
        if (treeNode.parent) {
            return getNodePath(treeNode.parent) + '.' + treeNode.path
        } else {
            return treeNode.path
        }
    }

    export function solveTreeNode(treeNode: ITreeNodeContent, parent?: ITreeNode): ITreeNode {
        if (!isNode(treeNode)) {
            treeNode = createNodeFromContent(treeNode, parent)
        }
        treeNode.children = treeNode.children.filter(child => {
            if (isWildcard(child)) {
                treeNode.data = child.data
                return false
            }
            return true
        }).map(child => solveTreeNode(child, treeNode as ITreeNode))
        if (treeNode.path && !treeNode.data) {
            console.warn("No data for path: " + getNodePath(treeNode as ITreeNode))
        }
        return treeNode as ITreeNode
    }

    export function parseMenuRoute(route: Route): IWeakNodeData[] {
        const result: IWeakNodeData[] = []
        if (route.children) {
            route.children.forEach(child => {
                result.push(...parseMenuRoute(child))
            })
        }
        if (route.path == '/') {
            console.warn("Root route is not allowed for resources expression.")
            return []
        }
        result.push({
            displayName: route.name,
            expression: `${route.path.slice(1).replace(/\//g, '.')}.*`,
            checked: false
        })
        return result
    }

    export function getAllowedMenu(treeNode: ITreeNode, ...expression: string[]): Set<string> {
        const st = new Set<string>()
        expression.forEach(exp => {
            const matched = treeNode.matchNode(exp)
            if (matched) {
                getAllowedExpression(matched, (exp) => {
                    st.add(exp)
                })
            }
        })
        return st
    }

    function getAllowedExpression(treeNode: ITreeNode, consumer: (expression: string) => void) {
        if (treeNode.data?.expression) {
            consumer(treeNode.data.expression)
        }
        treeNode.children.forEach(child => {
            getAllowedExpression(child as ITreeNode, consumer)
        })
    }

    export function getCheckedNodes(treeNode: ITreeNode): ITreeNode[] {
        const result: ITreeNode[] = []
        if (treeNode.data?.checked) {
            result.push(treeNode)
        }
        treeNode.children.forEach(child => {
            result.push(...getCheckedNodes(child as ITreeNode))
        })
        return result
    }

    export function getCheckedParent(treeNode: ITreeNode): ITreeNode[] {
        const result: ITreeNode[] = []
        if (treeNode.data?.checked) {
            return [treeNode]
        }
        treeNode.children.forEach(child => {
            return result.push(...getCheckedParent(child as ITreeNode))
        })
        return result
    }
}
